/*
 * Copyright 2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.pajamas.starter.core.util;

import com.google.common.collect.Sets;
import com.google.common.primitives.Booleans;

import org.apache.commons.collections4.CollectionUtils;
import org.pajamas.protocol.Matcher;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Comparator.comparing;

/**
 * @author william
 * @since 2024/3/7
 */
public final class StreamOptUtils {
    private StreamOptUtils() {
    }

    public static final class Sorting {
        private Sorting() {
        }

        public static <T> Comparator<T> noOp() {
            return (param1, param2) -> 0;
        }

        public static <T> Comparator<T> byPredicate(Predicate<T> predicate, boolean fst) {
            // IDE tips: marked as unstable with @Beta
            return comparing(predicate::test, fst ? Booleans.trueFirst() : Booleans.falseFirst());
        }

        public static <T> Comparator<T> byPredicate(Predicate<T> predicate) {
            return byPredicate(predicate, Boolean.TRUE);
        }

        public static <P, R> Comparator<P> byEnums(Function<P, R> keyExtractor, Set<R> enums, Matcher<R> matcher) {
            return (param1, param2) -> {
                R res1 = keyExtractor.apply(param1);
                R res2 = keyExtractor.apply(param2);
                int idx1 = locateEnumsIdx(res1, enums, matcher);
                int idx2 = locateEnumsIdx(res2, enums, matcher);
                return idx1 - idx2;
            };
        }

        private static <T> int locateEnumsIdx(T val, Set<T> enums, Matcher<T> matcher) {
            int idx = 0;
            for (T anEnum : enums) {
                if (matcher.isA(val, anEnum)) {
                    return idx;
                }
                idx++;
            }
            return idx;
        }
    }

    public static final class Filtering {
        private Filtering() {
        }

        public static <T, K> Predicate<T> noneMatch(Stream<T> stream, Function<T, K> keyExtractor) {
            return value -> {
                K key = Objects.requireNonNull(keyExtractor).apply(value);
                return key != null && Objects
                        .requireNonNull(stream)
                        .filter(Objects::nonNull)
                        .map(keyExtractor)
                        .noneMatch(key::equals);
            };
        }

        public static <T, K> Predicate<T> anyMatch(Stream<T> stream, Function<T, K> keyExtractor) {
            return value -> {
                K key = Objects.requireNonNull(keyExtractor).apply(value);
                return key != null && Objects
                        .requireNonNull(stream)
                        .filter(Objects::nonNull)
                        .map(keyExtractor)
                        .anyMatch(key::equals);
            };
        }

        public static <T, K> Set<T> difference(Collection<T> existing, Collection<T> noneExisting,
                                               Function<T, K> keyExtractor) {
            if (CollectionUtils.isEmpty(existing)) {
                return Collections.emptySet();
            }
            if (CollectionUtils.isEmpty(noneExisting)) {
                return Sets.newHashSet(existing);
            }
            return existing
                    .stream()
                    .filter(Objects::nonNull)
                    .filter(value -> noneMatch(noneExisting.stream(), keyExtractor).test(value))
                    .collect(Collectors.toSet());
        }

        public static <T, K> Set<T> intersection(Collection<T> collection1, Collection<T> collection2,
                                                 Function<T, K> keyExtractor) {
            if (CollectionUtils.isEmpty(collection1) || CollectionUtils.isEmpty(collection2)) {
                return Collections.emptySet();
            }
            return collection1
                    .stream()
                    .filter(Objects::nonNull)
                    .filter(value -> anyMatch(collection2.stream(), keyExtractor).test(value))
                    .collect(Collectors.toSet());
        }
    }
}

